package com.doudou.feign;

import feign.Contract;
import feign.Feign;
import feign.InvocationHandlerFactory;
import feign.Target;
import feign.hystrix.FallbackFactory;
import feign.hystrix.HystrixDelegatingContract;
import feign.hystrix.SetterFactory;
import org.springframework.beans.BeansException;
import org.springframework.cloud.openfeign.FeignContext;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * @Author: 傻男人
 * @Date: 2020/5/25 11:28
 * @Version: 1.0
 * @Description: 自定义feign注册类
 */
public class CustomFeignBuilder extends feign.Feign.Builder implements ApplicationContextAware {
    private Contract contract = new Contract.Default();
    private ApplicationContext applicationContext;
    private FeignContext feignContext;
    private SetterFactory setterFactory = new feign.hystrix.SetterFactory.Default();

    @Override
    public Feign.Builder invocationHandlerFactory(InvocationHandlerFactory invocationHandlerFactory) {
        throw new UnsupportedOperationException();
    }

    @Override
    public CustomFeignBuilder contract(Contract contract) {
        this.contract = contract;
        return this;
    }

    @Override
    public Feign build() {
        super.invocationHandlerFactory(new InvocationHandlerFactory() {
            @Override
            public InvocationHandler create(Target target,
                                            Map<Method, MethodHandler> dispatch) {
                Object feignClientFactoryBean = applicationContext.getBean("&" + target.type().getName());

                Class fallback = (Class) getFieldValue(feignClientFactoryBean, "fallback");
                Class fallbackFactory = (Class) getFieldValue(feignClientFactoryBean, "fallbackFactory");
                String name = (String) getFieldValue(feignClientFactoryBean, "name");

                Object fallbackInstance;
                FallbackFactory fallbackFactoryInstance;
                // check fallback and fallbackFactory properties
                if (void.class != fallback) {
                    fallbackInstance = getFromContext(name, "fallback", fallback, target.type());

                    return new HystrixInvocationHandler(
                            target,
                            dispatch,
                            setterFactory,
                            new FallbackFactory.Default(fallbackInstance)
                    );
                }
                if (void.class != fallbackFactory) {
                    fallbackFactoryInstance = (FallbackFactory) getFromContext(
                            name,
                            "fallbackFactory",
                            fallbackFactory,
                            FallbackFactory.class
                    );
                    doCheckFallBackFactory(fallbackFactoryInstance, target);
                    return new HystrixInvocationHandler(
                            target,
                            dispatch,
                            setterFactory,
                            fallbackFactoryInstance
                    );
                }

                fallbackFactoryInstance = buildDefaultHystrixFallbackFactory(target);
                doCheckFallBackFactory(fallbackFactoryInstance, target);
                return new HystrixInvocationHandler(target, dispatch, setterFactory, fallbackFactoryInstance);
            }


            private void doCheckFallBackFactory(FallbackFactory<?> fallbackFactory, Target target) {

                Object exampleFallback = fallbackFactory.create(new RuntimeException());
                Assert.notNull(exampleFallback,
                        String.format(
                                "Incompatible fallbackFactory instance for feign client %s. Factory may not produce null!",
                                target.type()));
                if (!target.type().isAssignableFrom(exampleFallback.getClass())) {
                    throw new IllegalStateException(
                            String.format(
                                    "Incompatible fallbackFactory instance for feign client %s. Factory produces instances of '%s', but should produce instances of '%s'",
                                    target.type(), exampleFallback.getClass(), target.type()));

                }

            }

            private DefaultHystrixFallbackFactory buildDefaultHystrixFallbackFactory(Target target) {
                DefaultHystrixFallbackHandlerFactory defaultHystrixFallbackHandlerFactory = new DefaultHystrixFallbackHandlerFactory();
                DefaultHystrixFallbackFactory defaultHystrixFallbackFactory = new DefaultHystrixFallbackFactory();
                defaultHystrixFallbackFactory.setFallbackHandlerFactory(defaultHystrixFallbackHandlerFactory);
                defaultHystrixFallbackFactory.setFeignClientClass(target.type());
                return defaultHystrixFallbackFactory;
            }


            private Object getFromContext(String name, String type,
                                          Class fallbackType, Class targetType) {
                Object fallbackInstance = feignContext.getInstance(name, fallbackType);
                if (fallbackInstance == null) {
                    throw new IllegalStateException(String.format(
                            "No %s instance of type %s found for feign client %s",
                            type, fallbackType, name));
                }

                if (!targetType.isAssignableFrom(fallbackType)) {
                    throw new IllegalStateException(String.format(
                            "Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s",
                            type, fallbackType, targetType, name));
                }
                return fallbackInstance;
            }
        });
        super.contract(new HystrixDelegatingContract(contract));
        return super.build();
    }

    private Object getFieldValue(Object instance, String fieldName) {
        Field field = ReflectionUtils.findField(instance.getClass(), fieldName);
        field.setAccessible(true);
        try {
            return field.get(instance);
        } catch (IllegalAccessException e) {
            // ignore
        }
        return null;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException {
        this.applicationContext = applicationContext;
        feignContext = this.applicationContext.getBean(FeignContext.class);
    }

}
